/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package simple.utils.data;

import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author Tom
 */
public class Bytes {

    public static byte[] stringToBytes(String s) {
        return s.getBytes(StandardCharsets.UTF_8);
    }

    public static String bytesToString(byte[] bytes, int offset) {
        return new String(bytes, offset, bytes.length - offset, StandardCharsets.UTF_8);
    }

    public static String bytesToString(byte[] bytes) {
        return new String(bytes, StandardCharsets.UTF_8);
    }

    public static byte[] doubleToBytes(double d) {
        return ByteBuffer.wrap(new byte[8]).putDouble(d).array();
    }

    public static double bytesToDouble(byte[] bytes) {
        Preconditions.checkArgument(bytes.length == 8);
        return ByteBuffer.wrap(bytes).getDouble();
    }

    public static double bytesToDouble(byte[] bytes, int offset) {
        return ByteBuffer.wrap(bytes, offset, 8).getDouble();
    }

    public static byte[] intToBytes(int i) {
        return ByteBuffer.wrap(new byte[4]).putInt(i).array();
    }

    public static int bytesToInt(byte[] bytes) {
        Preconditions.checkArgument(bytes.length == 4);
        return ByteBuffer.wrap(bytes).getInt();
    }

    public static byte[] longToBytes(long l) {
        return ByteBuffer.wrap(new byte[8]).putLong(l).array();
    }

    public static long bytesToLong(byte[] bytes) {
        Preconditions.checkArgument(bytes.length == 8);
        return ByteBuffer.wrap(bytes).getLong();
    }

    public static long bytesToLong(byte[] bytes, int offset) {
        return ByteBuffer.wrap(bytes, offset, 8).getLong();
    }

    public static int bytesToInt(byte[] bytes, int offset) {
        return ByteBuffer.wrap(bytes, offset, 4).getInt();
    }

    /**
     * Reads next length-delimited (4 bytes) record from input stream
     *
     * @param in
     * @return record or null if EOF (not even length)
     * @throws java.io.IOException
     */
    public static byte[] readNext(InputStream in) throws IOException {
        int length = readLength(in);
        if (length == -1) {
            return null;
        }
        byte[] buf = new byte[length];
        ByteStreams.readFully(in, buf);
        return buf;
    }

    private static int readLength(InputStream in) throws IOException {
        final int SIZE = 4;
        byte[] lb = new byte[SIZE];
        int total = 0;
        while (total < SIZE) {
            int read = in.read(lb, total, SIZE - total);
            if (read == -1) {
                // EOF
                if (total == 0) {
                    return -1;
                } else {
                    throw new IOException("Partial length!");
                }
            }
            total += read;
        }
        return Bytes.bytesToInt(lb);
    }

    /**
     * Writes data bytes to input stream, prepending 4 bytes of data length. Can be readed with readNext, readLengthDelimitedByteArrays
     *
     * @param out
     * @param data
     * @throws java.io.IOException
     */
    public static void writeLengthDelimited(OutputStream out, byte[] data) throws IOException {
        out.write(intToBytes(data.length));
        out.write(data);
    }

    /**
     * Reads input stream as length-delimited chunks of bytes... first 4 bytes are length, then data bytes, then 4 bytes of length, then data bytes, etc...
     *
     * @param in
     * @return
     * @throws java.io.IOException
     */
    public static List<byte[]> readLengthDelimitedByteArrays(InputStream in) throws IOException {
        List<byte[]> got = new ArrayList<>();
        byte[] b;
        while ((b = readNext(in)) != null) {
            got.add(b);
        }
        return got;
    }

    private static byte[] readNext(int length, InputStream in) throws IOException {
        byte[] buf = new byte[length];
        ByteStreams.readFully(in, buf);
        return buf;
    }

    public static void writeInt(OutputStream out, int num) throws IOException {
        out.write(intToBytes(num));
    }

    public static void writeLong(OutputStream out, long num) throws IOException {
        out.write(longToBytes(num));
    }

    public static void writeDouble(OutputStream out, double num) throws IOException {
        out.write(doubleToBytes(num));
    }

    public static int readInt(InputStream in) throws IOException {
        return bytesToInt(readNext(4, in));
    }

    public static long readNextLong(InputStream in) throws IOException {
        return bytesToLong(readNext(8, in));
    }

    public static double readDouble(InputStream in) throws IOException {
        return bytesToDouble(readNext(8, in));
    }

    public static int readNextInt(ByteArrayInputStream in) throws IOException {
        return bytesToInt(readNext(4, in));
    }

    public static byte[] intSetToBytes(Set<Integer> set) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        for (Integer i : set) {
            if (i != null) {
                os.write(intToBytes(i));
            }
        }
        os.close();
        return os.toByteArray();

    }

    public static Set<Integer> bytesToIntSet(byte[] bytes) {
        Set<Integer> s = new HashSet<>();
        for (int i = 0; i < bytes.length; i += 4) {
            byte[] bb = Arrays.copyOfRange(bytes, i, i + 4);
            s.add(bytesToInt(bb));
        }
        return s;
    }

    public static byte[] shortToBytes(short t) {
        return ByteBuffer.wrap(new byte[2]).putShort(t).array();
    }

    public static short bytesToShort(byte[] bytes) {
        Preconditions.checkArgument(bytes.length == 2);
        return ByteBuffer.wrap(bytes).getShort();
    }

    public static byte[] boolToBytes(Boolean aBoolean) {
        if (aBoolean) {
            return new byte[]{1};
        } else {
            return new byte[]{0};
        }
    }

    public static boolean bytesToBool(byte[] bytes) {
        Preconditions.checkArgument(bytes.length == 1);
        if (bytes[0] == 1) {
            return true;
        } else if (bytes[0] == 0) {
            return false;
        } else {
            throw new IllegalArgumentException("Unknown value " + bytes[0]);
        }
    }


    public static byte[] floatToBytes(float aFloat) {
        return ByteBuffer.wrap(new byte[4]).putFloat(aFloat).array();
    }

    public static Float bytesToFloat(byte[] bytes) {
        Preconditions.checkArgument(bytes.length == 4);
        return ByteBuffer.wrap(bytes).getFloat();
    }

    public static byte[] charToBytes(char character) {
        return ByteBuffer.wrap(new byte[2]).putChar(character).array();
    }

    public static char bytesToChar(byte[] bytes) {
        Preconditions.checkArgument(bytes.length == 2);
        return ByteBuffer.wrap(bytes).getChar();
    }
}
